package solver;

import java.util.List;
import model.BalanceObjective;
import model.Location;
import model.Machine;
import model.Neighborhood;
import model.Problem;
import model.Resource;
import model.Service;
import model.Solution;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;

public class OrtLPModelBuilder {
	Problem problem;
	int P, M, R, L, S, N, BO;
	Solution solution;
	boolean verbose = true;

	MPVariable[][] x;
	MPVariable[][] y;
	MPVariable[][] k;
	MPVariable[][] lo_cost;
	MPVariable[][][] a;
	MPVariable[][] abs;
	MPVariable[][] ba_cost;
	MPVariable pm_cost;
	MPVariable mm_cost;
	MPVariable[] se;
	MPVariable[] abs_se;
	MPVariable[] se_cost;
	MPSolver solver;
	double infinity = MPSolver.infinity();

	long load_cost;
	long balance_cost;
	long process_move_cost;
	long service_move_cost;
	long machine_move_cost;

	static {
		System.loadLibrary("jnilinearsolver");
	}

	public OrtLPModelBuilder(Problem problem, boolean verbose) {
		this.problem = problem;
		this.verbose = verbose;
		R = problem.getResources().size();
		P = problem.getProcesses().size();
		M = problem.getMachines().size();
		L = problem.getLocations().size();
		S = problem.getServices().size();
		N = problem.getNeighborhoods().size();
		BO = problem.getBalanceObjectives().size();

		x = new MPVariable[P][M];
		y = new MPVariable[S][L];
		k = new MPVariable[M][R];
		a = new MPVariable[BO][M][2];
		abs = new MPVariable[BO][M];
		ba_cost = new MPVariable[BO][M];
		se = new MPVariable[S];
		abs_se = new MPVariable[S - 1];
		se_cost = new MPVariable[S - 1];
		lo_cost = new MPVariable[M][R];
		solver = new MPSolver("RoadDefLinearExample",
				MPSolver.CBC_MIXED_INTEGER_PROGRAMMING);
	}

	public void setSolution(Solution solution) {
		this.solution = solution;
	}

	public void solve() {
		addVariables();
		addConstraints0();
		addConstraints1();
		addConstraints2();
		addConstraints3();
		addConstraints4();
		addConstraints5();
		addSoftConstraints0();
		addSoftConstraints1();
		addSoftConstraints2();
		addSoftConstraints3();
		addSoftConstraints4();
		// solver.EnableOutput();
		solver.solve();
		System.out.printf("Objective value=%.0f\n", solver.objectiveValue());
		load_cost = computeLoadCost();
		balance_cost = computeBalanceCost();
		process_move_cost = computeProcessMoveCost();
		service_move_cost = computeServiceMoveCost();
		machine_move_cost = computeMachineMoveCost();
		if (verbose) {
			System.out.printf(
					"[ORT Solution=%d] load_cost=%d balance_cost=%d process move cost=%d "
							+ "service move cost=%d machine move cost=%d\n",
					(long) Math.round(solver.objectiveValue()), load_cost,
					balance_cost, process_move_cost, service_move_cost,
					machine_move_cost);
		}
	}

	private long computeLoadCost() {
		long sum = 0;
		for (int m = 0; m < M; m++) {
			long sum2 = 0;
			for (int r = 0; r < R; r++) {
				long r_value = Math.round(lo_cost[m][r].solutionValue());
				Resource aResource = problem.getResourceByIndex(r);
				sum2 += r_value * aResource.getWeightLoadCost();
			}
			// System.out.printf("machine:%d load_cost:%d\n", m, sum2);
			sum += sum2;
		}
		return sum;
	}

	private long computeBalanceCost() {
		long sum = 0;
		int bo_index = 0;
		for (BalanceObjective bo : problem.getBalanceObjectives()) {
			for (int m = 0; m < M; m++) {
				long r_value = Math.round(ba_cost[bo_index][m].solutionValue());
				sum += r_value * bo.weightBalanceCost;
			}
		}
		return sum;
	}

	private long computeProcessMoveCost() {
		return (long) Math.round(pm_cost.solutionValue())
				* problem.getWeightProcessMoveCost();
	}

	private long computeServiceMoveCost() {
		long sum = (long) Math.round(se_cost[S - 2].solutionValue())
				* problem.getWeightServiceMoveCost();
		return sum;
	}

	private long computeMachineMoveCost() {
		return (long) Math.round(mm_cost.solutionValue())
				* problem.getWeightMachineMoveCost();
	}

	private void addVariables() {
		for (int i = 0; i < P; i++)
			for (int j = 0; j < M; j++) {
				x[i][j] = solver.makeBoolVar(String.format("x%d_%d", i, j));
			}
		for (int s = 0; s < S; s++)
			for (int l = 0; l < L; l++) {
				y[s][l] = solver.makeBoolVar(String.format("y%d_%d", s, l));
			}
		for (int m = 0; m < M; m++)
			for (int r = 0; r < R; r++) {
				k[m][r] = solver.makeIntVar(0, infinity,
						String.format("k%d_%d", m, r));
				lo_cost[m][r] = solver.makeIntVar(0, infinity,
						String.format("lc%d_%d", m, r));
				Resource resource = problem.getResources().get(r);
				solver.setObjectiveCoefficient(lo_cost[m][r],
						resource.getWeightLoadCost());
			}
		for (int bo_index = 0; bo_index < BO; bo_index++) {
			BalanceObjective bo = problem.getBalanceObjectives().get(bo_index);
			for (int m = 0; m < M; m++) {
				abs[bo_index][m] = solver.makeNumVar(0.0, infinity,
						String.format("abs_bo%d_m%d", bo_index, m));

				ba_cost[bo_index][m] = solver.makeNumVar(0.0, infinity,
						String.format("ba_cost_bo%d_m%d", bo_index, m));
				solver.setObjectiveCoefficient(ba_cost[bo_index][m],
						bo.weightBalanceCost);

				a[bo_index][m][0] = solver.makeIntVar(0.0, infinity, String
						.format("a_bo%d_m%d_r%d", bo_index, m, bo.resourcea));
				a[bo_index][m][1] = solver.makeIntVar(0.0, infinity, String
						.format("a_bo%d_m%d_r%d", bo_index, m, bo.resourceb));
			}
		}
		pm_cost = solver.makeNumVar(0, infinity, "pm_cost");
		solver.setObjectiveCoefficient(pm_cost, problem.getWeightProcessMoveCost());

		mm_cost = solver.makeIntVar(0, infinity, "mm_cost");
		solver.setObjectiveCoefficient(mm_cost, problem.getWeightMachineMoveCost());

		for (int s = 0; s < S; s++) {
			se[s] = solver
					.makeIntVar(0.0, infinity, String.format("se_s%d", s));
		}
		for (int s = 0; s < S - 1; s++) {
			abs_se[s] = solver.makeIntVar(0.0, infinity,
					String.format("abs_se_s%d", s));
			if (s == S - 2) {
				se_cost[s] = solver.makeIntVar(0.0, infinity,
						String.format("se_cost_s%d", s));
				solver.setObjectiveCoefficient(se_cost[s],
						problem.getWeightServiceMoveCost());
			} else
				se_cost[s] = solver.makeIntVar(0.0, infinity,
						String.format("se_cost_s%d", s));
		}
		solver.setMinimization();
	}

	private void addConstraints0() {
		for (int i = 0; i < P; i++) {
			MPConstraint c = solver.makeConstraint(1, 1,
					String.format("assignment_constraint_p%d", i));
			for (int j = 0; j < M; j++) {
				c.setCoefficient(x[i][j], 1);
			}
		}
	}

	private void addConstraints1() {
		for (int m = 0; m < M; m++) {
			for (int r = 0; r < R; r++) {
				int capacity = problem.C[m][r];
				MPConstraint c = solver.makeConstraint(0, capacity,
						String.format("capacity_constr_m%d_r%d", m, r));
				for (int p = 0; p < P; p++) {
					c.setCoefficient(x[p][m], problem.R[p][r]);
				}
			}
		}
	}

	private void addConstraints2() {
		int k = 0;
		for (Service service : problem.getServices()) {
			List<Integer> processes = service.getProcesses();
			if (processes.size() == 1)
				continue;
			else {
				for (int m = 0; m < M; m++) {
					MPConstraint c = solver.makeConstraint(0, 1.0,
							String.format("conflict_constr_sa_%d", k));
					for (Integer p : processes) {
						c.setCoefficient(x[p][m], 1.0);
					}
					k++;
				}
			}
		}
	}

	// spread constraints
	private void addConstraints3() {
		int k = 0;
		for (Service service : problem.getServices()) {
			if (service.getSpreadMin() == 0) {
				k++;
				continue;
			}
			MPConstraint c = solver.makeConstraint(service.getSpreadMin(),
					infinity, String.format("spread_constr_s%d", k));
			for (Location location : problem.getLocations()) {
				MPConstraint c1 = solver.makeConstraint(
						0.0,
						infinity,
						String.format("spread_constr_s%d_l%d_a", k,
								location.getId()));
				MPConstraint c2 = solver.makeConstraint(
						-infinity,
						0.0,
						String.format("spread_constr_s%d_l%d_b", k,
								location.getId()));
				for (Integer m : location.getMachines()) {
					for (Integer p : service.getProcesses()) {
						c1.setCoefficient(x[p][m], 1.0);
						c2.setCoefficient(x[p][m], 1.0);
					}
				}
				c1.setCoefficient(y[k][location.getId()], -1.0);
				c2.setCoefficient(y[k][location.getId()], -location.getMachines()
						.size());
				c.setCoefficient(y[k][location.getId()], 1.0);
			}
			k++;
		}
	}

	// Dependency constraints
	private void addConstraints4() {
		int sa_index = 0;
		for (Service servicea : problem.getServices()) {
			if (servicea.getDependsOn().isEmpty()) {
				sa_index++;
				continue;
			}
			for (Integer sb_index : servicea.getDependsOn()) {
				Service serviceb = problem.getServices().get(sb_index);
				for (Integer pa_index : servicea.getProcesses()) {
					for (Neighborhood neighborhood : problem.getNeighborhoods()) {
						MPConstraint c = solver.makeConstraint(-infinity, 0.0,
								String.format(
										"depends_constr_sa%d_sb%d_p%d_n%d",
										sa_index, sb_index, pa_index,
										neighborhood.getId()));
						for (Integer m_nb : neighborhood.getMachines()) {
							c.setCoefficient(x[pa_index][m_nb], 1.0);
							for (Integer pb_index : serviceb.getProcesses()) {
								c.setCoefficient(x[pb_index][m_nb], -1.0);
							}
						}
					}
				}
			}
			sa_index++;
		}
	}

	// Transient constraints
	private void addConstraints5() {
		for (int r = 0; r < R; r++) {
			Resource resource = problem.getResourceByIndex(r);
			if (!resource.isTransientUsage())
				continue;
			for (Integer m : solution.getMachinesInSolution()) {
				int capacity = problem.C[m][r];
				for (int p = 0; p < P; p++) {
					if (solution.getMachineIdForProcessId(p) == m) {
						capacity -= problem.R[p][r];
					}
				}
				MPConstraint c = solver.makeConstraint(-infinity, capacity,
						String.format("transient_constr_m%d_r%d", m, r));
				for (int p = 0; p < P; p++) {
					if (solution.getMachineIdForProcessId(p) != m) {
						c.setCoefficient(x[p][m], problem.R[p][r]);
					}
				}
			}
		}
	}

	// load cost
	private void addSoftConstraints0() {
		for (int r = 0; r < R; r++) {
			for (int m = 0; m < M; m++) {
				int safety_capacity = problem.getMachines().get(m)
						.getSafetyCapacityFor("r" + r);
				MPConstraint c1 = solver.makeConstraint(safety_capacity,
						safety_capacity,
						String.format("load_cost_constr_m%d_r%d_a", m, r));
				MPConstraint c2 = solver.makeConstraint(0, infinity,
						String.format("load_cost_constr_m%d_r%d_b", m, r));
				MPConstraint c3 = solver.makeConstraint(0, infinity,
						String.format("load_cost_constr_m%d_r%d_c", m, r));
				for (int p = 0; p < P; p++) {
					c1.setCoefficient(x[p][m], problem.R[p][r] / 2.0);
					c2.setCoefficient(x[p][m], -problem.R[p][r]);
					c3.setCoefficient(x[p][m], problem.R[p][r]);
				}
				c1.setCoefficient(k[m][r], 0.5);
				c1.setCoefficient(lo_cost[m][r], -1.0);
				c2.setCoefficient(k[m][r], 1.0);
				c3.setCoefficient(k[m][r], 1.0);
			}
		}
	}

	// balance cost
	private void addSoftConstraints1() {
		int bo_index = 0;
		for (BalanceObjective bo : problem.getBalanceObjectives()) {
			for (int m = 0; m < M; m++) {
				int capacity_a = problem.getMachines().get(m)
						.getCapacityFor("r" + bo.resourcea);
				int capacity_b = problem.getMachines().get(m)
						.getCapacityFor("r" + bo.resourceb);
				MPConstraint c1a = solver.makeConstraint(capacity_a,
						capacity_a, String.format(
								"balance_cost_constr_bo%d_m%d_r%d_a%d_%d",
								bo_index, m, bo.resourcea, m, bo.resourcea));
				MPConstraint c1b = solver.makeConstraint(capacity_b,
						capacity_b, String.format(
								"balance_cost_constr_bo%d_m%d_r%d_a%d_%d",
								bo_index, m, bo.resourceb, m, bo.resourceb));
				for (int p = 0; p < P; p++) {
					c1a.setCoefficient(x[p][m], problem.R[p][bo.resourcea]);
					c1b.setCoefficient(x[p][m], problem.R[p][bo.resourceb]);
				}
				c1a.setCoefficient(a[bo_index][m][0], 1.0);
				c1b.setCoefficient(a[bo_index][m][1], 1.0);
			}
			for (int m = 0; m < M; m++) {
				MPConstraint c2 = solver.makeConstraint(0.0, 0.0, String
						.format("balance_cost_constr_bo%d_m%d_a", bo_index, m));
				c2.setCoefficient(a[bo_index][m][0], bo.target / 2.0);
				c2.setCoefficient(a[bo_index][m][1], -0.5);
				c2.setCoefficient(abs[bo_index][m], 0.5);
				c2.setCoefficient(ba_cost[bo_index][m], -1.0);

				MPConstraint c3 = solver.makeConstraint(0.0, infinity, String
						.format("balance_cost_constr_bo%d_m%d_b", bo_index, m));
				c3.setCoefficient(abs[bo_index][m], 1.0);
				c3.setCoefficient(a[bo_index][m][0], bo.target);
				c3.setCoefficient(a[bo_index][m][1], -1.0);

				MPConstraint c4 = solver.makeConstraint(0.0, infinity, String
						.format("balance_cost_constr_bo%d_m%d_c", bo_index, m));
				c4.setCoefficient(abs[bo_index][m], 1.0);
				c4.setCoefficient(a[bo_index][m][0], -bo.target);
				c4.setCoefficient(a[bo_index][m][1], 1.0);
			}
			bo_index++;
		}
	}

	// process move cost
	private void addSoftConstraints2() {
		int sum = 0;
		for (int p = 0; p < P; p++) {
			int pmc = problem.getProcesses().get(p).getProcessMoveCost();
			for (int m = 0; m < M; m++) {
				if (solution.getMachineIdForProcessId(p) == m) {
					sum += pmc;
				}
			}
		}
		MPConstraint c = solver.makeConstraint(-sum, -sum, "process_move_cost");
		for (int p = 0; p < P; p++) {
			int pmc = problem.getProcesses().get(p).getProcessMoveCost();
			for (int m = 0; m < M; m++) {
				if (solution.getMachineIdForProcessId(p) == m) {
					c.setCoefficient(x[p][m], -pmc);
				} else {
					c.setCoefficient(x[p][m], pmc);
				}
			}
		}
		c.setCoefficient(pm_cost, -2.0);
	}

	// service move cost
	private void addSoftConstraints3() {
		if (S <= 1)
			return;
		for (int s = 0; s < S; s++) {
			Service service = problem.getServices().get(s);
			MPConstraint c = solver.makeConstraint(-service.getProcesses()
					.size(), -service.getProcesses().size(), String.format(
					"service_move_cost_constr_s%d", s));
			for (Integer p : service.getProcesses()) {
				for (int m = 0; m < M; m++) {
					if (solution.getMachineIdForProcessId(p) == m) {
						c.setCoefficient(x[p][m], -1.0);
					} else {
						c.setCoefficient(x[p][m], 1.0);
					}
				}
			}
			c.setCoefficient(se[s], -2.0);
		}

		MPConstraint c1 = solver.makeConstraint(0, 0,
				String.format("service_move_cost_constr_max_s0_a"));
		c1.setCoefficient(se[0], 0.5);
		c1.setCoefficient(se[1], 0.5);
		c1.setCoefficient(abs_se[0], 0.5);
		c1.setCoefficient(se_cost[0], -1.0);

		MPConstraint c2 = solver.makeConstraint(0, 0,
				String.format("service_move_cost_constr_max_s0_b"));
		c2.setCoefficient(abs_se[0], 1.0);
		c2.setCoefficient(se[0], 1.0);
		c2.setCoefficient(se[1], -1.0);

		MPConstraint c3 = solver.makeConstraint(0, infinity,
				String.format("service_move_cost_constr_max_s0_c"));
		c3.setCoefficient(abs_se[0], 1.0);
		c3.setCoefficient(se[0], -1.0);
		c3.setCoefficient(se[1], 1.0);

		for (int s = 2; s < S; s++) {
			MPConstraint c4 = solver.makeConstraint(0, 0,
					String.format("service_move_cost_constr_max_s%d_a", s));
			c4.setCoefficient(se_cost[s - 2], 0.5);
			c4.setCoefficient(se[s], 0.5);
			c4.setCoefficient(abs_se[s - 1], 0.5);
			c4.setCoefficient(se_cost[s - 1], -1.0);

			MPConstraint c5 = solver.makeConstraint(0, infinity,
					String.format("service_move_cost_constr_max_s%d_b", s));
			c5.setCoefficient(abs_se[s - 1], 1.0);
			c5.setCoefficient(se_cost[s - 2], 1.0);
			c5.setCoefficient(se[s], -1.0);

			MPConstraint c6 = solver.makeConstraint(0, infinity,
					String.format("service_move_cost_constr_max_s%d_c", s));
			c6.setCoefficient(abs_se[s - 1], 1.0);
			c6.setCoefficient(se_cost[s - 2], -1.0);
			c6.setCoefficient(se[s], 1.0);
		}
	}

	// machine move cost
	private void addSoftConstraints4() {
		MPConstraint c = solver.makeConstraint(0, 0, "machine_move_cost");
		for (int p = 0; p < P; p++) {
			int msource = solution.getMachineIdForProcessId(p);
			Machine aMachine = problem.getMachines().get(msource);
			for (int mdest = 0; mdest < M; mdest++) {
				if (msource != mdest) {
					c.setCoefficient(x[p][mdest], aMachine.getMachineMoveCost(mdest));
				}
			}
		}
		c.setCoefficient(mm_cost, -1.0);
	}

	public Solution saveSolution() {
		Solution solution = new Solution(problem);
		for (int p = 0; p < P; p++) {
			for (int m = 0; m < M; m++) {
				double x_value = x[p][m].solutionValue();
				if (x_value > 0.9999) {
					solution.assignProcessToMachine(p, m);
				}
			}
		}
		return solution;
	}
}
